diff --git a/src/net.h b/src/net.h
--- a/src/net.h
+++ b/src/net.h
@@ -1064,8 +1064,10 @@
      * Attempts to open a connection. Currently only used from tests.
      *
      * @param[in]   address     Address of node to try connecting to
-     * @param[in]   conn_type   ConnectionType::OUTBOUND or
-     * ConnectionType::BLOCK_RELAY or ConnectionType::ADDR_FETCH
+     * @param[in]   conn_type   ConnectionType::OUTBOUND,
+     *                          ConnectionType::BLOCK_RELAY,
+     *                          ConnectionType::ADDR_FETCH, or
+     *                          ConnectionType::FEELER
      * @return      bool        Returns false if there are no available
      *                          slots for this connection:
      *                          - conn_type not a supported ConnectionType
diff --git a/src/net.cpp b/src/net.cpp
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -1310,7 +1310,6 @@
     switch (conn_type) {
         case ConnectionType::INBOUND:
         case ConnectionType::MANUAL:
-        case ConnectionType::FEELER:
             return false;
         case ConnectionType::OUTBOUND_FULL_RELAY:
             max_connections = m_max_outbound_full_relay;
@@ -1321,6 +1320,9 @@
         // no limit for ADDR_FETCH because -seednode has no limit either
         case ConnectionType::ADDR_FETCH:
             break;
+        // no limit for FEELER connections since they're short-lived
+        case ConnectionType::FEELER:
+            break;
     } // no default case, so the compiler can warn about missing cases
 
     // Count existing connections
diff --git a/src/net_processing.cpp b/src/net_processing.cpp
--- a/src/net_processing.cpp
+++ b/src/net_processing.cpp
@@ -1370,7 +1370,8 @@
             : CAddress(CService(), addr.nServices);
     CAddress addrMe = CAddress(CService(), nLocalNodeServices);
 
-    const bool tx_relay = !m_ignore_incoming_txs && pnode.m_tx_relay != nullptr;
+    const bool tx_relay = !m_ignore_incoming_txs &&
+                          pnode.m_tx_relay != nullptr && !pnode.IsFeelerConn();
     m_connman.PushMessage(
         &pnode, CNetMsgMaker(INIT_PROTO_VERSION)
                     .Make(NetMsgType::VERSION, PROTOCOL_VERSION,
diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp
--- a/src/rpc/net.cpp
+++ b/src/rpc/net.cpp
@@ -399,7 +399,7 @@
              "The IP address and port to attempt connecting to."},
             {"connection_type", RPCArg::Type::STR, RPCArg::Optional::NO,
              "Type of connection to open (\"outbound-full-relay\", "
-             "\"block-relay-only\" or \"addr-fetch\")."},
+             "\"block-relay-only\", \"addr-fetch\" or \"feeler\")."},
         },
         RPCResult{RPCResult::Type::OBJ,
                   "",
@@ -434,6 +434,8 @@
                 conn_type = ConnectionType::BLOCK_RELAY;
             } else if (conn_type_in == "addr-fetch") {
                 conn_type = ConnectionType::ADDR_FETCH;
+            } else if (conn_type_in == "feeler") {
+                conn_type = ConnectionType::FEELER;
             } else {
                 throw JSONRPCError(RPC_INVALID_PARAMETER, self.ToString());
             }
diff --git a/test/functional/p2p_add_connections.py b/test/functional/p2p_add_connections.py
--- a/test/functional/p2p_add_connections.py
+++ b/test/functional/p2p_add_connections.py
@@ -15,6 +15,15 @@
     assert_equal(info["connections_out"], num_out)
 
 
+class P2PFeelerReceiver(P2PInterface):
+    def on_version(self, message):
+        # The bitcoind node closes feeler connections as soon as a version
+        # message is received from the test framework. Don't send any responses
+        # to the node's version message since the connection will already be
+        # closed.
+        pass
+
+
 class P2PAddConnections(BitcoinTestFramework):
     def set_test_params(self):
         self.num_nodes = 2
@@ -99,6 +108,18 @@
 
         check_node_connections(node=self.nodes[1], num_in=5, num_out=10)
 
+        self.log.info("Add 1 feeler connection to node 0")
+        feeler_conn = self.nodes[0].add_outbound_p2p_connection(
+            P2PFeelerReceiver(), p2p_idx=6, connection_type="feeler")
+
+        # Feeler connection is closed
+        assert not feeler_conn.is_connected
+
+        # Verify version message received
+        assert_equal(feeler_conn.message_count["version"], 1)
+        # Feeler connections do not request tx relay
+        assert_equal(feeler_conn.last_message["version"].relay, 0)
+
 
 if __name__ == '__main__':
     P2PAddConnections().main()
diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py
--- a/test/functional/test_framework/test_node.py
+++ b/test/functional/test_framework/test_node.py
@@ -722,7 +722,7 @@
     def add_outbound_p2p_connection(
             self, p2p_conn, *, p2p_idx, connection_type="outbound-full-relay", **kwargs):
         """Add an outbound p2p connection from node. Must be an
-        "outbound-full-relay", "block-relay-only" or "addr-fetch" connection.
+        "outbound-full-relay", "block-relay-only", "addr-fetch" or "feeler" connection.
 
         This method adds the p2p connection to the self.p2ps list and returns
         the connection to the caller.
@@ -740,11 +740,21 @@
             timeout_factor=self.timeout_factor,
             **kwargs)()
 
-        p2p_conn.wait_for_connect()
-        self.p2ps.append(p2p_conn)
+        if connection_type == "feeler":
+            # feeler connections are closed as soon as the node receives a
+            # `version` message
+            p2p_conn.wait_until(
+                lambda: p2p_conn.message_count["version"] == 1,
+                check_connected=False)
+            p2p_conn.wait_until(
+                lambda: not p2p_conn.is_connected,
+                check_connected=False)
+        else:
+            p2p_conn.wait_for_connect()
+            self.p2ps.append(p2p_conn)
 
-        p2p_conn.wait_for_verack()
-        p2p_conn.sync_with_ping()
+            p2p_conn.wait_for_verack()
+            p2p_conn.sync_with_ping()
 
         return p2p_conn