diff --git a/Cargo.lock b/Cargo.lock
index 03993fa614c2b5867c0a6d1577d3738bd07bd230..bd7a29278a3e05f0466664fbbbc9b68640829122 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1997,6 +1997,7 @@ dependencies = [
  "pallet-bridge-messages",
  "pallet-message-queue",
  "pallet-xcm",
+ "pallet-xcm-bridge-hub",
  "parachains-common",
  "parity-scale-codec",
  "rococo-system-emulated-network",
@@ -2012,6 +2013,7 @@ dependencies = [
  "staging-xcm",
  "staging-xcm-executor",
  "testnet-parachains-constants",
+ "xcm-runtime-apis",
 ]
 
 [[package]]
@@ -2179,11 +2181,14 @@ dependencies = [
  "pallet-bridge-messages",
  "pallet-message-queue",
  "pallet-xcm",
+ "pallet-xcm-bridge-hub",
  "parachains-common",
  "rococo-westend-system-emulated-network",
+ "sp-core",
  "sp-runtime",
  "staging-xcm",
  "staging-xcm-executor",
+ "xcm-runtime-apis",
 ]
 
 [[package]]
diff --git a/bridges/modules/xcm-bridge-hub-router/src/lib.rs b/bridges/modules/xcm-bridge-hub-router/src/lib.rs
index 8b86029ab8ffb06ab57b57dcbbfc22f5df36ab10..3c56f54a72bc57a7f3cdffb5aced0d1a1b4528e7 100644
--- a/bridges/modules/xcm-bridge-hub-router/src/lib.rs
+++ b/bridges/modules/xcm-bridge-hub-router/src/lib.rs
@@ -96,7 +96,7 @@ pub mod pallet {
 		/// Origin of the sibling bridge hub that is allowed to report bridge status.
 		type BridgeHubOrigin: EnsureOrigin<Self::RuntimeOrigin>;
 		/// Actual message sender (`HRMP` or `DMP`) to the sibling bridge hub location.
-		type ToBridgeHubSender: SendXcm + InspectMessageQueues;
+		type ToBridgeHubSender: SendXcm;
 		/// Underlying channel with the sibling bridge hub. It must match the channel, used
 		/// by the `Self::ToBridgeHubSender`.
 		type WithBridgeHubChannel: XcmChannelStatusProvider;
@@ -398,12 +398,12 @@ impl<T: Config<I>, I: 'static> SendXcm for Pallet<T, I> {
 }
 
 impl<T: Config<I>, I: 'static> InspectMessageQueues for Pallet<T, I> {
-	fn clear_messages() {
-		ViaBridgeHubExporter::<T, I>::clear_messages()
-	}
+	fn clear_messages() {}
 
+	/// This router needs to implement `InspectMessageQueues` but doesn't have to
+	/// return any messages, since it just reuses the `XcmpQueue` router.
 	fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
-		ViaBridgeHubExporter::<T, I>::get_messages()
+		Vec::new()
 	}
 }
 
@@ -648,34 +648,13 @@ mod tests {
 	}
 
 	#[test]
-	fn get_messages_works() {
+	fn get_messages_does_not_return_anything() {
 		run_test(|| {
 			assert_ok!(send_xcm::<XcmBridgeHubRouter>(
 				(Parent, Parent, GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)).into(),
 				vec![ClearOrigin].into()
 			));
-			assert_eq!(
-				XcmBridgeHubRouter::get_messages(),
-				vec![(
-					VersionedLocation::V4((Parent, Parachain(1002)).into()),
-					vec![VersionedXcm::V4(
-						Xcm::builder()
-							.withdraw_asset((Parent, 1_002_000))
-							.buy_execution((Parent, 1_002_000), Unlimited)
-							.set_appendix(
-								Xcm::builder_unsafe()
-									.deposit_asset(AllCounted(1), (Parent, Parachain(1000)))
-									.build()
-							)
-							.export_message(
-								Kusama,
-								Parachain(1000),
-								Xcm::builder_unsafe().clear_origin().build()
-							)
-							.build()
-					)],
-				),],
-			);
+			assert_eq!(XcmBridgeHubRouter::get_messages(), vec![]);
 		});
 	}
 }
diff --git a/cumulus/pallets/parachain-system/src/lib.rs b/cumulus/pallets/parachain-system/src/lib.rs
index 7f0bfceff2528fcc1021709416030a19ebcf7275..6f8adaa461c5d481d489300010d17e769c6558e4 100644
--- a/cumulus/pallets/parachain-system/src/lib.rs
+++ b/cumulus/pallets/parachain-system/src/lib.rs
@@ -1623,7 +1623,11 @@ impl<T: Config> InspectMessageQueues for Pallet<T> {
 			.map(|encoded_message| VersionedXcm::<()>::decode(&mut &encoded_message[..]).unwrap())
 			.collect();
 
-		vec![(VersionedLocation::V4(Parent.into()), messages)]
+		if messages.is_empty() {
+			vec![]
+		} else {
+			vec![(VersionedLocation::from(Location::parent()), messages)]
+		}
 	}
 }
 
diff --git a/cumulus/parachains/integration-tests/emulated/common/src/macros.rs b/cumulus/parachains/integration-tests/emulated/common/src/macros.rs
index 6f6bbe41e01bd208ee6d40a9f3b4ba8f98f7975b..f12907de914a5001bf4c35bb1cfeb01f718c5377 100644
--- a/cumulus/parachains/integration-tests/emulated/common/src/macros.rs
+++ b/cumulus/parachains/integration-tests/emulated/common/src/macros.rs
@@ -130,3 +130,323 @@ macro_rules! test_parachain_is_trusted_teleporter {
 		}
 	};
 }
+
+#[macro_export]
+macro_rules! test_relay_is_trusted_teleporter {
+	( $sender_relay:ty, $sender_xcm_config:ty, vec![$( $receiver_para:ty ),+], ($assets:expr, $amount:expr) ) => {
+		$crate::macros::paste::paste! {
+			// init Origin variables
+			let sender = [<$sender_relay Sender>]::get();
+			let mut relay_sender_balance_before =
+				<$sender_relay as $crate::macros::Chain>::account_data_of(sender.clone()).free;
+			let origin = <$sender_relay as $crate::macros::Chain>::RuntimeOrigin::signed(sender.clone());
+			let fee_asset_item = 0;
+			let weight_limit = $crate::macros::WeightLimit::Unlimited;
+
+			$(
+				{
+					// init Destination variables
+					let receiver = [<$receiver_para Receiver>]::get();
+					let para_receiver_balance_before =
+						<$receiver_para as $crate::macros::Chain>::account_data_of(receiver.clone()).free;
+					let para_destination =
+						<$sender_relay>::child_location_of(<$receiver_para>::para_id());
+					let beneficiary: Location =
+						$crate::macros::AccountId32 { network: None, id: receiver.clone().into() }.into();
+
+					// Send XCM message from Relay
+					<$sender_relay>::execute_with(|| {
+						assert_ok!(<$sender_relay as [<$sender_relay Pallet>]>::XcmPallet::limited_teleport_assets(
+							origin.clone(),
+							bx!(para_destination.clone().into()),
+							bx!(beneficiary.clone().into()),
+							bx!($assets.clone().into()),
+							fee_asset_item,
+							weight_limit.clone(),
+						));
+
+						type RuntimeEvent = <$sender_relay as $crate::macros::Chain>::RuntimeEvent;
+
+						assert_expected_events!(
+							$sender_relay,
+							vec![
+								RuntimeEvent::XcmPallet(
+									$crate::macros::pallet_xcm::Event::Attempted { outcome: Outcome::Complete { .. } }
+								) => {},
+								RuntimeEvent::Balances(
+									$crate::macros::pallet_balances::Event::Burned { who: sender, amount }
+								) => {},
+								RuntimeEvent::XcmPallet(
+									$crate::macros::pallet_xcm::Event::Sent { .. }
+								) => {},
+							]
+						);
+					});
+
+					// Receive XCM message in Destination Parachain
+					<$receiver_para>::execute_with(|| {
+						type RuntimeEvent = <$receiver_para as $crate::macros::Chain>::RuntimeEvent;
+
+						assert_expected_events!(
+							$receiver_para,
+							vec![
+								RuntimeEvent::Balances(
+									$crate::macros::pallet_balances::Event::Minted { who: receiver, .. }
+								) => {},
+								RuntimeEvent::MessageQueue(
+									$crate::macros::pallet_message_queue::Event::Processed { success: true, .. }
+								) => {},
+							]
+						);
+					});
+
+					// Check if balances are updated accordingly in Origin and Parachain
+					let relay_sender_balance_after =
+						<$sender_relay as $crate::macros::Chain>::account_data_of(sender.clone()).free;
+					let para_receiver_balance_after =
+						<$receiver_para as $crate::macros::Chain>::account_data_of(receiver.clone()).free;
+					let delivery_fees = <$sender_relay>::execute_with(|| {
+						$crate::macros::asset_test_utils::xcm_helpers::teleport_assets_delivery_fees::<
+							<$sender_xcm_config as xcm_executor::Config>::XcmSender,
+						>($assets.clone(), fee_asset_item, weight_limit.clone(), beneficiary, para_destination)
+					});
+
+					assert_eq!(relay_sender_balance_before - $amount - delivery_fees, relay_sender_balance_after);
+					assert!(para_receiver_balance_after > para_receiver_balance_before);
+
+					// Update sender balance
+					relay_sender_balance_before = <$sender_relay as $crate::macros::Chain>::account_data_of(sender.clone()).free;
+				}
+			)+
+		}
+	};
+}
+
+#[macro_export]
+macro_rules! test_parachain_is_trusted_teleporter_for_relay {
+	( $sender_para:ty, $sender_xcm_config:ty, $receiver_relay:ty, $amount:expr ) => {
+		$crate::macros::paste::paste! {
+			// init Origin variables
+			let sender = [<$sender_para Sender>]::get();
+			let para_sender_balance_before =
+				<$sender_para as $crate::macros::Chain>::account_data_of(sender.clone()).free;
+			let origin = <$sender_para as $crate::macros::Chain>::RuntimeOrigin::signed(sender.clone());
+			let assets: Assets = (Parent, $amount).into();
+			let fee_asset_item = 0;
+			let weight_limit = $crate::macros::WeightLimit::Unlimited;
+
+			// init Destination variables
+			let receiver = [<$receiver_relay Receiver>]::get();
+			let relay_receiver_balance_before =
+				<$receiver_relay as $crate::macros::Chain>::account_data_of(receiver.clone()).free;
+			let relay_destination: Location = Parent.into();
+			let beneficiary: Location =
+				$crate::macros::AccountId32 { network: None, id: receiver.clone().into() }.into();
+
+			// Send XCM message from Parachain
+			<$sender_para>::execute_with(|| {
+				assert_ok!(<$sender_para as [<$sender_para Pallet>]>::PolkadotXcm::limited_teleport_assets(
+					origin.clone(),
+					bx!(relay_destination.clone().into()),
+					bx!(beneficiary.clone().into()),
+					bx!(assets.clone().into()),
+					fee_asset_item,
+					weight_limit.clone(),
+				));
+
+				type RuntimeEvent = <$sender_para as $crate::macros::Chain>::RuntimeEvent;
+
+				assert_expected_events!(
+					$sender_para,
+					vec![
+						RuntimeEvent::PolkadotXcm(
+							$crate::macros::pallet_xcm::Event::Attempted { outcome: Outcome::Complete { .. } }
+						) => {},
+						RuntimeEvent::Balances(
+							$crate::macros::pallet_balances::Event::Burned { who: sender, amount }
+						) => {},
+						RuntimeEvent::PolkadotXcm(
+							$crate::macros::pallet_xcm::Event::Sent { .. }
+						) => {},
+					]
+				);
+			});
+
+			// Receive XCM message in Destination Parachain
+			<$receiver_relay>::execute_with(|| {
+				type RuntimeEvent = <$receiver_relay as $crate::macros::Chain>::RuntimeEvent;
+
+				assert_expected_events!(
+					$receiver_relay,
+					vec![
+						RuntimeEvent::Balances(
+							$crate::macros::pallet_balances::Event::Minted { who: receiver, .. }
+						) => {},
+						RuntimeEvent::MessageQueue(
+							$crate::macros::pallet_message_queue::Event::Processed { success: true, .. }
+						) => {},
+					]
+				);
+			});
+
+			// Check if balances are updated accordingly in Origin and Relay Chain
+			let para_sender_balance_after =
+				<$sender_para as $crate::macros::Chain>::account_data_of(sender.clone()).free;
+			let relay_receiver_balance_after =
+				<$receiver_relay as $crate::macros::Chain>::account_data_of(receiver.clone()).free;
+			let delivery_fees = <$sender_para>::execute_with(|| {
+				$crate::macros::asset_test_utils::xcm_helpers::teleport_assets_delivery_fees::<
+					<$sender_xcm_config as xcm_executor::Config>::XcmSender,
+				>(assets, fee_asset_item, weight_limit.clone(), beneficiary, relay_destination)
+			});
+
+			assert_eq!(para_sender_balance_before - $amount - delivery_fees, para_sender_balance_after);
+			assert!(relay_receiver_balance_after > relay_receiver_balance_before);
+		}
+	};
+}
+
+#[macro_export]
+macro_rules! test_chain_can_claim_assets {
+	( $sender_para:ty, $runtime_call:ty, $network_id:expr, $assets:expr, $amount:expr ) => {
+		$crate::macros::paste::paste! {
+			let sender = [<$sender_para Sender>]::get();
+			let origin = <$sender_para as $crate::macros::Chain>::RuntimeOrigin::signed(sender.clone());
+			// Receiver is the same as sender
+			let beneficiary: Location =
+				$crate::macros::AccountId32 { network: Some($network_id), id: sender.clone().into() }.into();
+			let versioned_assets: $crate::macros::VersionedAssets = $assets.clone().into();
+
+			<$sender_para>::execute_with(|| {
+				// Assets are trapped for whatever reason.
+				// The possible reasons for this might differ from runtime to runtime, so here we just drop them directly.
+				<$sender_para as [<$sender_para Pallet>]>::PolkadotXcm::drop_assets(
+					&beneficiary,
+					$assets.clone().into(),
+					&XcmContext { origin: None, message_id: [0u8; 32], topic: None },
+				);
+
+				type RuntimeEvent = <$sender_para as $crate::macros::Chain>::RuntimeEvent;
+				assert_expected_events!(
+					$sender_para,
+					vec![
+						RuntimeEvent::PolkadotXcm(
+							$crate::macros::pallet_xcm::Event::AssetsTrapped { origin: beneficiary, assets: versioned_assets, .. }
+						) => {},
+					]
+				);
+
+				let balance_before = <$sender_para as [<$sender_para Pallet>]>::Balances::free_balance(&sender);
+
+				// Different origin or different assets won't work.
+				let other_origin = <$sender_para as $crate::macros::Chain>::RuntimeOrigin::signed([<$sender_para Receiver>]::get());
+				assert!(<$sender_para as [<$sender_para Pallet>]>::PolkadotXcm::claim_assets(
+					other_origin,
+					bx!(versioned_assets.clone().into()),
+					bx!(beneficiary.clone().into()),
+				).is_err());
+				let other_versioned_assets: $crate::macros::VersionedAssets = Assets::new().into();
+				assert!(<$sender_para as [<$sender_para Pallet>]>::PolkadotXcm::claim_assets(
+					origin.clone(),
+					bx!(other_versioned_assets.into()),
+					bx!(beneficiary.clone().into()),
+				).is_err());
+
+				// Assets will be claimed to `beneficiary`, which is the same as `sender`.
+				assert_ok!(<$sender_para as [<$sender_para Pallet>]>::PolkadotXcm::claim_assets(
+					origin.clone(),
+					bx!(versioned_assets.clone().into()),
+					bx!(beneficiary.clone().into()),
+				));
+
+				assert_expected_events!(
+					$sender_para,
+					vec![
+						RuntimeEvent::PolkadotXcm(
+							$crate::macros::pallet_xcm::Event::AssetsClaimed { origin: beneficiary, assets: versioned_assets, .. }
+						) => {},
+					]
+				);
+
+				// After claiming the assets, the balance has increased.
+				let balance_after = <$sender_para as [<$sender_para Pallet>]>::Balances::free_balance(&sender);
+				assert_eq!(balance_after, balance_before + $amount);
+
+				// Claiming the assets again doesn't work.
+				assert!(<$sender_para as [<$sender_para Pallet>]>::PolkadotXcm::claim_assets(
+					origin.clone(),
+					bx!(versioned_assets.clone().into()),
+					bx!(beneficiary.clone().into()),
+				).is_err());
+
+				let balance = <$sender_para as [<$sender_para Pallet>]>::Balances::free_balance(&sender);
+				assert_eq!(balance, balance_after);
+
+				// You can also claim assets and send them to a different account.
+				<$sender_para as [<$sender_para Pallet>]>::PolkadotXcm::drop_assets(
+					&beneficiary,
+					$assets.clone().into(),
+					&XcmContext { origin: None, message_id: [0u8; 32], topic: None },
+				);
+				let receiver = [<$sender_para Receiver>]::get();
+				let other_beneficiary: Location =
+					$crate::macros::AccountId32 { network: Some($network_id), id: receiver.clone().into() }.into();
+				let balance_before = <$sender_para as [<$sender_para Pallet>]>::Balances::free_balance(&receiver);
+				assert_ok!(<$sender_para as [<$sender_para Pallet>]>::PolkadotXcm::claim_assets(
+					origin.clone(),
+					bx!(versioned_assets.clone().into()),
+					bx!(other_beneficiary.clone().into()),
+				));
+				let balance_after = <$sender_para as [<$sender_para Pallet>]>::Balances::free_balance(&receiver);
+				assert_eq!(balance_after, balance_before + $amount);
+			});
+		}
+	};
+}
+
+#[macro_export]
+macro_rules! test_dry_run_transfer_across_pk_bridge {
+	( $sender_asset_hub:ty, $sender_bridge_hub:ty, $destination:expr ) => {
+		$crate::macros::paste::paste! {
+			use frame_support::{dispatch::RawOrigin, traits::fungible};
+			use sp_runtime::AccountId32;
+			use xcm::prelude::*;
+			use xcm_runtime_apis::dry_run::runtime_decl_for_dry_run_api::DryRunApiV1;
+
+			let who = AccountId32::new([1u8; 32]);
+			let transfer_amount = 10_000_000_000_000u128;
+			let initial_balance = transfer_amount * 10;
+
+			// Bridge setup.
+			$sender_asset_hub::force_xcm_version($destination, XCM_VERSION);
+
+			<$sender_asset_hub as TestExt>::execute_with(|| {
+				type Runtime = <$sender_asset_hub as Chain>::Runtime;
+				type RuntimeCall = <$sender_asset_hub as Chain>::RuntimeCall;
+				type OriginCaller = <$sender_asset_hub as Chain>::OriginCaller;
+				type Balances = <$sender_asset_hub as [<$sender_asset_hub Pallet>]>::Balances;
+
+				// Give some initial funds.
+				<Balances as fungible::Mutate<_>>::set_balance(&who, initial_balance);
+
+				let call = RuntimeCall::PolkadotXcm(pallet_xcm::Call::transfer_assets {
+					dest: Box::new(VersionedLocation::from($destination)),
+					beneficiary: Box::new(VersionedLocation::from(Junction::AccountId32 {
+						id: who.clone().into(),
+						network: None,
+					})),
+					assets: Box::new(VersionedAssets::from(vec![
+						(Parent, transfer_amount).into(),
+					])),
+					fee_asset_item: 0,
+					weight_limit: Unlimited,
+				});
+				let result = Runtime::dry_run_call(OriginCaller::system(RawOrigin::Signed(who)), call).unwrap();
+				// We assert the dry run succeeds and sends only one message to the local bridge hub.
+				assert!(result.execution_result.is_ok());
+				assert_eq!(result.forwarded_xcms.len(), 1);
+				assert_eq!(result.forwarded_xcms[0].0, VersionedLocation::from(Location::new(1, [Parachain($sender_bridge_hub::para_id().into())])));
+			});
+		}
+	};
+}
diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/Cargo.toml b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/Cargo.toml
index 0eb99c64c801cc56adb1a0f4e788e3fb0f3c1b77..9938ec3ab623ae9bc8ce57df0739383a73d5369a 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/Cargo.toml
+++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/Cargo.toml
@@ -14,6 +14,7 @@ workspace = true
 codec = { workspace = true }
 scale-info = { features = ["derive"], workspace = true }
 hex-literal = { workspace = true, default-features = true }
+
 sp-core.workspace = true
 frame-support.workspace = true
 pallet-assets.workspace = true
@@ -25,7 +26,9 @@ sp-runtime.workspace = true
 xcm.workspace = true
 pallet-xcm.workspace = true
 xcm-executor.workspace = true
+xcm-runtime-apis.workspace = true
 pallet-bridge-messages.workspace = true
+pallet-xcm-bridge-hub.workspace = true
 cumulus-pallet-xcmp-queue.workspace = true
 emulated-integration-tests-common.workspace = true
 parachains-common.workspace = true
diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/lib.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/lib.rs
index 04466a611c71318280d135324ba3de418f9348d9..ac6cdf9257e8ca9bd953f8b18dd1b736fcc322f4 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/lib.rs
+++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/lib.rs
@@ -31,7 +31,7 @@ mod imports {
 	pub use emulated_integration_tests_common::{
 		accounts::ALICE,
 		impls::Inspect,
-		test_parachain_is_trusted_teleporter,
+		test_dry_run_transfer_across_pk_bridge, test_parachain_is_trusted_teleporter,
 		xcm_emulator::{
 			assert_expected_events, bx, Chain, Parachain as Para, RelayChain as Relay, TestExt,
 		},
diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/asset_transfers.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/asset_transfers.rs
index 6053936487b26e97fe8575cd5b666a03cff7ccf6..91062b247134517453d19c8f4eaae9bd3f1f4a27 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/asset_transfers.rs
+++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/asset_transfers.rs
@@ -517,3 +517,12 @@ fn send_back_wnds_from_penpal_rococo_through_asset_hub_rococo_to_asset_hub_weste
 	assert!(receiver_wnds_after > receiver_wnds_before);
 	assert!(receiver_wnds_after <= receiver_wnds_before + amount);
 }
+
+#[test]
+fn dry_run_transfer_to_westend_sends_xcm_to_bridge_hub() {
+	test_dry_run_transfer_across_pk_bridge!(
+		AssetHubRococo,
+		BridgeHubRococo,
+		asset_hub_westend_location()
+	);
+}
diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/Cargo.toml b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/Cargo.toml
index 71559162f37e9b27fb38f318784174dfb723d9ff..e23247adad515dd4b367b70d10008e1006b40b10 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/Cargo.toml
+++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/Cargo.toml
@@ -18,11 +18,14 @@ pallet-asset-conversion.workspace = true
 pallet-balances.workspace = true
 pallet-message-queue.workspace = true
 pallet-message-queue.default-features = true
+sp-core.workspace = true
 sp-runtime.workspace = true
 xcm.workspace = true
 pallet-xcm.workspace = true
 xcm-executor.workspace = true
+xcm-runtime-apis.workspace = true
 pallet-bridge-messages.workspace = true
+pallet-xcm-bridge-hub.workspace = true
 cumulus-pallet-xcmp-queue.workspace = true
 emulated-integration-tests-common.workspace = true
 parachains-common.workspace = true
diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/lib.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/lib.rs
index 3b0fcea57a26f3e06080c28e9d6495f1b71e3680..90e473dbdd6fc5cbaefeae21d0561033180c1f94 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/lib.rs
+++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/lib.rs
@@ -32,7 +32,7 @@ mod imports {
 	pub use emulated_integration_tests_common::{
 		accounts::ALICE,
 		impls::Inspect,
-		test_parachain_is_trusted_teleporter,
+		test_dry_run_transfer_across_pk_bridge, test_parachain_is_trusted_teleporter,
 		xcm_emulator::{
 			assert_expected_events, bx, Chain, Parachain as Para, RelayChain as Relay, TestExt,
 		},
diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/asset_transfers.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/asset_transfers.rs
index 0c0b04cd45a91c42592052799ba9580bd6e4219a..a2107275e6a28113181b4143e8da86d24321597f 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/asset_transfers.rs
+++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/asset_transfers.rs
@@ -543,3 +543,12 @@ fn send_back_rocs_from_penpal_westend_through_asset_hub_westend_to_asset_hub_roc
 	assert!(receiver_rocs_after > receiver_rocs_before);
 	assert!(receiver_rocs_after <= receiver_rocs_before + amount);
 }
+
+#[test]
+fn dry_run_transfer_to_rococo_sends_xcm_to_bridge_hub() {
+	test_dry_run_transfer_across_pk_bridge!(
+		AssetHubWestend,
+		BridgeHubWestend,
+		asset_hub_rococo_location()
+	);
+}
diff --git a/polkadot/xcm/xcm-builder/src/universal_exports.rs b/polkadot/xcm/xcm-builder/src/universal_exports.rs
index 30e0b7c72b03e4a93b6ce35995c2e9df1bb27212..5c754f01ec0ad2b5ebdd29a4ab4b3cc4649ea393 100644
--- a/polkadot/xcm/xcm-builder/src/universal_exports.rs
+++ b/polkadot/xcm/xcm-builder/src/universal_exports.rs
@@ -337,15 +337,15 @@ impl<Bridges: ExporterFor, Router: SendXcm, UniversalLocation: Get<InteriorLocat
 	}
 }
 
-impl<Bridges, Router: InspectMessageQueues, UniversalLocation> InspectMessageQueues
+impl<Bridges, Router, UniversalLocation> InspectMessageQueues
 	for SovereignPaidRemoteExporter<Bridges, Router, UniversalLocation>
 {
-	fn clear_messages() {
-		Router::clear_messages()
-	}
+	fn clear_messages() {}
 
+	/// This router needs to implement `InspectMessageQueues` but doesn't have to
+	/// return any messages, since it just reuses the `XcmpQueue` router.
 	fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
-		Router::get_messages()
+		Vec::new()
 	}
 }
 
diff --git a/prdoc/pr_6004.prdoc b/prdoc/pr_6004.prdoc
new file mode 100644
index 0000000000000000000000000000000000000000..f50cd722c71469a128fed5f5bdb59d2fc25ca362
--- /dev/null
+++ b/prdoc/pr_6004.prdoc
@@ -0,0 +1,20 @@
+# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0
+# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json
+
+title: Remove redundant XCMs from dry run's forwarded xcms
+
+doc:
+  - audience: Runtime User
+    description: |
+      The DryRunApi was returning the same message repeated multiple times in the
+      `forwarded_xcms` field. This is no longer the case.
+
+crates:
+  - name: pallet-xcm-bridge-hub-router
+    bump: patch
+  - name: cumulus-pallet-parachain-system
+    bump: patch
+  - name: staging-xcm-builder
+    bump: patch
+  - name: emulated-integration-tests-common
+    bump: minor