diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/tests/tests.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/tests/tests.rs
index 9adfc821d83be056da7612d559d772ae65b10612..2969b47fca8231eb0f74481318c71a20a82a31fc 100644
--- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/tests/tests.rs
+++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/tests/tests.rs
@@ -19,7 +19,7 @@
 use bp_polkadot_core::Signature;
 use bridge_hub_rococo_runtime::{
 	bridge_common_config, bridge_to_bulletin_config, bridge_to_westend_config,
-	xcm_config::{RelayNetwork, TokenLocation, XcmConfig},
+	xcm_config::{LocationToAccountId, RelayNetwork, TokenLocation, XcmConfig},
 	AllPalletsWithoutSystem, BridgeRejectObsoleteHeadersAndMessages, EthereumGatewayAddress,
 	Executive, ExistentialDeposit, ParachainSystem, PolkadotXcm, Runtime, RuntimeCall,
 	RuntimeEvent, RuntimeOrigin, SessionKeys, SignedExtra, TransactionPayment, UncheckedExtrinsic,
@@ -148,7 +148,7 @@ mod bridge_hub_westend_tests {
 	use bridge_hub_test_utils::test_cases::from_parachain;
 	use bridge_to_westend_config::{
 		BridgeHubWestendLocation, WestendGlobalConsensusNetwork,
-		WithBridgeHubWestendMessagesInstance,
+		WithBridgeHubWestendMessagesInstance, XcmOverBridgeHubWestendInstance,
 	};
 
 	// Para id of sibling chain used in tests.
@@ -445,6 +445,23 @@ mod bridge_hub_westend_tests {
 			),
 		)
 	}
+
+	#[test]
+	fn open_and_close_bridge_work() {
+		let source = Location::new(1, [Parachain(2053)]);
+		let destination = [GlobalConsensus(NetworkId::Westend), Parachain(1075)].into();
+
+		bridge_hub_test_utils::test_cases::open_and_close_bridge_work::<
+			Runtime,
+			XcmOverBridgeHubWestendInstance,
+			LocationToAccountId,
+		>(
+			collator_session_keys(),
+			bp_bridge_hub_westend::BRIDGE_HUB_WESTEND_PARACHAIN_ID,
+			source,
+			destination,
+		)
+	}
 }
 
 mod bridge_hub_bulletin_tests {
@@ -453,7 +470,7 @@ mod bridge_hub_bulletin_tests {
 	use bridge_hub_test_utils::test_cases::from_grandpa_chain;
 	use bridge_to_bulletin_config::{
 		RococoBulletinGlobalConsensusNetwork, RococoBulletinGlobalConsensusNetworkLocation,
-		WithRococoBulletinMessagesInstance, XCM_LANE_FOR_ROCOCO_PEOPLE_TO_ROCOCO_BULLETIN,
+		WithRococoBulletinMessagesInstance, XcmOverPolkadotBulletinInstance,
 	};
 
 	// Para id of sibling chain used in tests.
@@ -586,4 +603,21 @@ mod bridge_hub_bulletin_tests {
 			construct_and_apply_extrinsic,
 		)
 	}
+
+	#[test]
+	fn open_and_close_bridge_work() {
+		let source = Location::new(1, [Parachain(2053)]);
+		let destination = [GlobalConsensus(RococoBulletinGlobalConsensusNetwork::get())].into();
+
+		bridge_hub_test_utils::test_cases::open_and_close_bridge_work::<
+			Runtime,
+			XcmOverPolkadotBulletinInstance,
+			LocationToAccountId,
+		>(
+			collator_session_keys(),
+			bp_bridge_hub_rococo::BRIDGE_HUB_ROCOCO_PARACHAIN_ID,
+			source,
+			destination,
+		)
+	}
 }
diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/tests/tests.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/tests/tests.rs
index 698bee80937b3ddfe68f2d9875a1445ace7cf07f..64e265579c13165ee2fc97d1f1ea874453bf5281 100644
--- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/tests/tests.rs
+++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/tests/tests.rs
@@ -21,14 +21,14 @@ use bridge_common_config::{DeliveryRewardInBalance, RequiredStakeForStakeAndSlas
 use bridge_hub_test_utils::{test_cases::from_parachain, SlotDurations};
 use bridge_hub_westend_runtime::{
 	bridge_common_config, bridge_to_rococo_config,
-	xcm_config::{RelayNetwork, WestendLocation, XcmConfig},
+	xcm_config::{LocationToAccountId, RelayNetwork, WestendLocation, XcmConfig},
 	AllPalletsWithoutSystem, BridgeRejectObsoleteHeadersAndMessages, Executive, ExistentialDeposit,
 	ParachainSystem, PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, SessionKeys,
 	SignedExtra, TransactionPayment, UncheckedExtrinsic,
 };
 use bridge_to_rococo_config::{
 	BridgeGrandpaRococoInstance, BridgeHubRococoLocation, BridgeParachainRococoInstance,
-	WithBridgeHubRococoMessagesInstance,
+	WithBridgeHubRococoMessagesInstance, XcmOverBridgeHubRococoInstance,
 };
 use codec::{Decode, Encode};
 use frame_support::{dispatch::GetDispatchInfo, parameter_types, traits::ConstU8};
@@ -326,3 +326,20 @@ pub fn can_calculate_fee_for_standalone_message_confirmation_transaction() {
 		),
 	)
 }
+
+#[test]
+fn open_and_close_bridge_work() {
+	let source = Location::new(1, [Parachain(2053)]);
+	let destination = [GlobalConsensus(NetworkId::Rococo), Parachain(1075)].into();
+
+	bridge_hub_test_utils::test_cases::open_and_close_bridge_work::<
+		Runtime,
+		XcmOverBridgeHubRococoInstance,
+		LocationToAccountId,
+	>(
+		collator_session_keys(),
+		bp_bridge_hub_westend::BRIDGE_HUB_WESTEND_PARACHAIN_ID,
+		source,
+		destination,
+	)
+}
diff --git a/cumulus/parachains/runtimes/bridge-hubs/test-utils/src/test_cases/mod.rs b/cumulus/parachains/runtimes/bridge-hubs/test-utils/src/test_cases/mod.rs
index cd5fef0e99d76d7320ea9e37e427ada3705e0f11..4120bff457465f5df311042ecdbe745626d22c6b 100644
--- a/cumulus/parachains/runtimes/bridge-hubs/test-utils/src/test_cases/mod.rs
+++ b/cumulus/parachains/runtimes/bridge-hubs/test-utils/src/test_cases/mod.rs
@@ -32,15 +32,14 @@ use bp_messages::{
 	LaneId, LaneState, MessageKey, MessagesOperatingMode, OutboundLaneData,
 };
 use bp_runtime::BasicOperatingMode;
-use bp_xcm_bridge_hub::XcmAsPlainPayload;
+use bp_xcm_bridge_hub::{Bridge, BridgeState, XcmAsPlainPayload};
 use codec::Encode;
 use frame_support::{
 	assert_ok,
 	dispatch::GetDispatchInfo,
-	traits::{Get, OnFinalize, OnInitialize, OriginTrait},
+	traits::{fungible::Mutate, Get, OnFinalize, OnInitialize, OriginTrait},
 };
 use frame_system::pallet_prelude::BlockNumberFor;
-use pallet_xcm_bridge_hub::XcmBlobMessageDispatchResult;
 use parachains_common::AccountId;
 use parachains_runtimes_test_utils::{
 	mock_open_hrmp_channel, AccountIdOf, BalanceOf, CollatorSessionKeys, ExtBuilder, RuntimeCallOf,
@@ -50,7 +49,7 @@ use sp_runtime::{traits::Zero, AccountId32};
 use xcm::{latest::prelude::*, AlwaysLatest};
 use xcm_builder::DispatchBlobError;
 use xcm_executor::{
-	traits::{TransactAsset, WeightBounds},
+	traits::{ConvertLocation, TransactAsset, WeightBounds},
 	XcmExecutor,
 };
 
@@ -58,10 +57,16 @@ use xcm_executor::{
 pub(crate) mod bridges_prelude {
 	pub use bp_parachains::{RelayBlockHash, RelayBlockNumber};
 	pub use pallet_bridge_grandpa::{Call as BridgeGrandpaCall, Config as BridgeGrandpaConfig};
-	pub use pallet_bridge_messages::{Call as BridgeMessagesCall, Config as BridgeMessagesConfig};
+	pub use pallet_bridge_messages::{
+		Call as BridgeMessagesCall, Config as BridgeMessagesConfig, LanesManagerError,
+	};
 	pub use pallet_bridge_parachains::{
 		Call as BridgeParachainsCall, Config as BridgeParachainsConfig,
 	};
+	pub use pallet_xcm_bridge_hub::{
+		Call as BridgeXcmOverBridgeCall, Config as BridgeXcmOverBridgeConfig, LanesManagerOf,
+		XcmBlobMessageDispatchResult,
+	};
 }
 
 // Re-export test_case from assets
@@ -641,3 +646,124 @@ where
 
 	estimated_fee.into()
 }
+
+/// Helper function to open the bridge/lane for `source` and `destination` while ensuring all
+/// required balances are placed into the SA of the source.
+pub fn ensure_opened_bridge<Runtime, XcmOverBridgePalletInstance, LocationToAccountId>(source: Location, destination: InteriorLocation,)
+where
+	Runtime: BasicParachainRuntime + BridgeXcmOverBridgeConfig<XcmOverBridgePalletInstance>,
+	XcmOverBridgePalletInstance: 'static,
+	<Runtime as frame_system::Config>::RuntimeCall: GetDispatchInfo + From<BridgeXcmOverBridgeCall<Runtime, XcmOverBridgePalletInstance>>,
+	<Runtime as pallet_balances::Config>::Balance: From<<<Runtime as pallet_bridge_messages::Config<<Runtime as pallet_xcm_bridge_hub::Config<XcmOverBridgePalletInstance>>::BridgeMessagesPalletInstance>>::ThisChain as bp_runtime::Chain>::Balance>,
+	<Runtime as pallet_balances::Config>::Balance: From<u128>,
+LocationToAccountId: ConvertLocation<AccountIdOf<Runtime>>{
+	// required balance: ED + fee + BridgeDeposit
+	let bridge_deposit =
+		<Runtime as pallet_xcm_bridge_hub::Config<XcmOverBridgePalletInstance>>::BridgeDeposit::get(
+		);
+	// random high enough value for `BuyExecution` fees
+	let buy_execution_fee_amount = 5_000_000_000_000_u128;
+	let buy_execution_fee = (Location::parent(), buy_execution_fee_amount).into();
+	let balance_needed = <Runtime as pallet_balances::Config>::ExistentialDeposit::get() +
+		buy_execution_fee_amount.into() +
+		bridge_deposit.into();
+
+	// SA of source location needs to have some required balance
+	let source_account_id = LocationToAccountId::convert_location(&source).expect("valid location");
+	let _ = <pallet_balances::Pallet<Runtime>>::mint_into(&source_account_id, balance_needed)
+		.expect("mint_into passes");
+
+	// open bridge with `Transact` call
+	let open_bridge_call = RuntimeCallOf::<Runtime>::from(BridgeXcmOverBridgeCall::<
+		Runtime,
+		XcmOverBridgePalletInstance,
+	>::open_bridge {
+		bridge_destination_universal_location: Box::new(destination.into()),
+	});
+
+	// execute XCM as source origin would do with `Transact -> Origin::Xcm`
+	assert_ok!(RuntimeHelper::<Runtime>::execute_as_origin_xcm(
+		open_bridge_call,
+		source.clone(),
+		buy_execution_fee
+	)
+	.ensure_complete());
+}
+
+/// Test-case makes sure that `Runtime` can open/close bridges.
+pub fn open_and_close_bridge_work<Runtime, XcmOverBridgePalletInstance, LocationToAccountId>(
+	collator_session_key: CollatorSessionKeys<Runtime>,
+	runtime_para_id: u32,
+	source: Location,
+	destination: InteriorLocation,
+) where
+	Runtime: BasicParachainRuntime + BridgeXcmOverBridgeConfig<XcmOverBridgePalletInstance>,
+	XcmOverBridgePalletInstance: 'static,
+	<Runtime as frame_system::Config>::RuntimeCall: GetDispatchInfo + From<BridgeXcmOverBridgeCall<Runtime, XcmOverBridgePalletInstance>>,
+	<Runtime as pallet_balances::Config>::Balance: From<<<Runtime as pallet_bridge_messages::Config<<Runtime as pallet_xcm_bridge_hub::Config<XcmOverBridgePalletInstance>>::BridgeMessagesPalletInstance>>::ThisChain as bp_runtime::Chain>::Balance>,
+	<Runtime as pallet_balances::Config>::Balance: From<u128>,
+	<<Runtime as pallet_bridge_messages::Config<<Runtime as pallet_xcm_bridge_hub::Config<XcmOverBridgePalletInstance>>::BridgeMessagesPalletInstance>>::ThisChain as bp_runtime::Chain>::AccountId: From<<Runtime as frame_system::Config>::AccountId>,
+	LocationToAccountId: ConvertLocation<AccountIdOf<Runtime>>,
+{
+	run_test::<Runtime, _>(collator_session_key, runtime_para_id, vec![], || {
+		// construct expected bridge configuration
+		let locations = pallet_xcm_bridge_hub::Pallet::<Runtime, XcmOverBridgePalletInstance>::bridge_locations(
+			source.clone().into(),
+			Box::new(destination.clone().into()),
+		).expect("valid bridge locations");
+		let lanes_manager = LanesManagerOf::<Runtime, XcmOverBridgePalletInstance>::new();
+
+		// check bridge/lane DOES not exist
+		assert_eq!(
+			pallet_xcm_bridge_hub::Bridges::<Runtime, XcmOverBridgePalletInstance>::get(
+				locations.bridge_id
+			),
+			None
+		);
+		assert_eq!(
+			lanes_manager.active_inbound_lane(locations.bridge_id.lane_id()).map(drop),
+			Err(LanesManagerError::UnknownInboundLane)
+		);
+		assert_eq!(
+			lanes_manager.active_outbound_lane(locations.bridge_id.lane_id()).map(drop),
+			Err(LanesManagerError::UnknownOutboundLane)
+		);
+
+		// open bridge with Transact call from sibling
+		ensure_opened_bridge::<Runtime, XcmOverBridgePalletInstance, LocationToAccountId>(
+			source.clone(),
+			destination,
+		);
+
+		// check bridge/lane DOES exist
+		assert_eq!(
+			pallet_xcm_bridge_hub::Bridges::<Runtime, XcmOverBridgePalletInstance>::get(
+				locations.bridge_id
+			),
+			Some(
+				Bridge {
+					bridge_origin_relative_location: Box::new(source.clone().into()),
+					state: BridgeState::Opened,
+					bridge_owner_account: LocationToAccountId::convert_location(&source)
+						.expect("valid location")
+						.into(),
+					reserve: <Runtime as pallet_xcm_bridge_hub::Config<
+						XcmOverBridgePalletInstance,
+					>>::BridgeDeposit::get(),
+				}
+			)
+		);
+		assert_eq!(
+			lanes_manager
+				.active_inbound_lane(locations.bridge_id.lane_id())
+				.map(|lane| lane.state()),
+			Ok(LaneState::Opened)
+		);
+		assert_eq!(
+			lanes_manager
+				.active_outbound_lane(locations.bridge_id.lane_id())
+				.map(|lane| lane.state()),
+			Ok(LaneState::Opened)
+		);
+	});
+}
diff --git a/cumulus/parachains/runtimes/test-utils/src/lib.rs b/cumulus/parachains/runtimes/test-utils/src/lib.rs
index 940aa1b734dfc856b5a72cf83cc14b5f8980bc5e..fe75b2b6e72f2254867c94253c38f2ebef718a92 100644
--- a/cumulus/parachains/runtimes/test-utils/src/lib.rs
+++ b/cumulus/parachains/runtimes/test-utils/src/lib.rs
@@ -22,7 +22,7 @@ use cumulus_primitives_core::{
 use cumulus_primitives_parachain_inherent::ParachainInherentData;
 use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder;
 use frame_support::{
-	dispatch::{DispatchResult, RawOrigin},
+	dispatch::{DispatchResult, GetDispatchInfo, RawOrigin},
 	inherent::{InherentData, ProvideInherent},
 	pallet_prelude::Get,
 	traits::{OnFinalize, OnInitialize, OriginTrait, UnfilteredDispatchable},
@@ -450,6 +450,7 @@ impl<
 				require_weight_at_most,
 				call: call.into(),
 			},
+			ExpectTransactStatus(MaybeErrorCode::Success),
 		]);
 
 		// execute xcm as parent origin
@@ -462,6 +463,38 @@ impl<
 			Weight::zero(),
 		)
 	}
+
+	pub fn execute_as_origin_xcm<Call: GetDispatchInfo + Encode>(
+		call: Call,
+		origin: Location,
+		buy_execution_fee: Asset,
+	) -> Outcome {
+		// prepare `Transact` xcm
+		let xcm = Xcm(vec![
+			WithdrawAsset(buy_execution_fee.clone().into()),
+			BuyExecution { fees: buy_execution_fee.clone(), weight_limit: Unlimited },
+			Transact {
+				origin_kind: OriginKind::Xcm,
+				require_weight_at_most: call.get_dispatch_info().weight,
+				call: call.encode().into(),
+			},
+			ExpectTransactStatus(MaybeErrorCode::Success),
+		]);
+
+		// execute xcm as parent origin
+		let mut hash = xcm.using_encoded(sp_io::hashing::blake2_256);
+		<<Runtime as pallet_xcm::Config>::XcmExecutor>::prepare_and_execute(
+			origin.clone(),
+			xcm,
+			&mut hash,
+			Self::xcm_max_weight(if origin == Location::parent() {
+				XcmReceivedFrom::Parent
+			} else {
+				XcmReceivedFrom::Sibling
+			}),
+			Weight::zero(),
+		)
+	}
 }
 
 pub enum XcmReceivedFrom {