diff --git a/Cargo.toml b/Cargo.toml
index ffbaa20c7a2..905814fa186 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -56,6 +56,8 @@ ruma = { git = "https://github.com/matrix-org/ruma", rev = "f25b3220d0c3ece77200
     "compat-encrypted-stickers",
     "unstable-msc3401",
     "unstable-msc3266",
+    "unstable-msc3488",
+    "unstable-msc3489",
     "unstable-msc4075",
     "unstable-msc4140",
 ] }
diff --git a/crates/matrix-sdk-base/src/rooms/mod.rs b/crates/matrix-sdk-base/src/rooms/mod.rs
index 4d530b73f51..9035ca2be9d 100644
--- a/crates/matrix-sdk-base/src/rooms/mod.rs
+++ b/crates/matrix-sdk-base/src/rooms/mod.rs
@@ -18,6 +18,7 @@ pub use normal::{
 use ruma::{
     assign,
     events::{
+        beacon_info::BeaconInfoEventContent,
         call::member::CallMemberEventContent,
         macros::EventContent,
         room::{
@@ -81,6 +82,9 @@ impl fmt::Display for DisplayName {
 pub struct BaseRoomInfo {
     /// The avatar URL of this room.
     pub(crate) avatar: Option<MinimalStateEvent<RoomAvatarEventContent>>,
+    /// All shared live location beacons of this room.
+    #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
+    pub(crate) beacons: BTreeMap<OwnedUserId, MinimalStateEvent<BeaconInfoEventContent>>,
     /// The canonical alias of this room.
     pub(crate) canonical_alias: Option<MinimalStateEvent<RoomCanonicalAliasEventContent>>,
     /// The `m.room.create` event content of this room.
@@ -141,6 +145,9 @@ impl BaseRoomInfo {
     /// Returns true if the event modified the info, false otherwise.
     pub fn handle_state_event(&mut self, ev: &AnySyncStateEvent) -> bool {
         match ev {
+            AnySyncStateEvent::BeaconInfo(b) => {
+                self.beacons.insert(b.state_key().clone(), b.into());
+            }
             // No redacted branch - enabling encryption cannot be undone.
             AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(encryption)) => {
                 self.encryption = Some(encryption.content.clone());
@@ -335,6 +342,7 @@ impl Default for BaseRoomInfo {
     fn default() -> Self {
         Self {
             avatar: None,
+            beacons: BTreeMap::new(),
             canonical_alias: None,
             create: None,
             dm_targets: Default::default(),
diff --git a/crates/matrix-sdk-base/src/store/migration_helpers.rs b/crates/matrix-sdk-base/src/store/migration_helpers.rs
index fc2332fd7ca..89633af1a2b 100644
--- a/crates/matrix-sdk-base/src/store/migration_helpers.rs
+++ b/crates/matrix-sdk-base/src/store/migration_helpers.rs
@@ -199,6 +199,7 @@ impl BaseRoomInfoV1 {
 
         Box::new(BaseRoomInfo {
             avatar,
+            beacons: BTreeMap::new(),
             canonical_alias,
             create,
             dm_targets,
diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs
index 79a9a6f887d..04ea4bd6257 100644
--- a/crates/matrix-sdk/src/error.rs
+++ b/crates/matrix-sdk/src/error.rs
@@ -467,6 +467,39 @@ pub enum ImageError {
     ThumbnailBiggerThanOriginal,
 }
 
+/// Errors that can happen when interacting with the beacon API.
+#[derive(Debug, Error)]
+pub enum BeaconError {
+    // A network error occurred.
+    #[error("Network error: {0}")]
+    Network(#[from] HttpError),
+
+    // The beacon information is not found.
+    #[error("Existing beacon information not found.")]
+    NotFound,
+
+    // The redacted event is not an error, but it's not useful for the client.
+    #[error("Beacon event is redacted and cannot be processed.")]
+    Redacted,
+
+    // The client must join the room to access the beacon information.
+    #[error("Must join the room to access beacon information.")]
+    Stripped,
+
+    #[error("Deserialization error: {0}")]
+    Deserialization(#[from] serde_json::Error),
+
+    // Allow for other errors to be wrapped.
+    #[error("Other error: {0}")]
+    Other(Box<Error>),
+}
+
+impl From<Error> for BeaconError {
+    fn from(err: Error) -> Self {
+        BeaconError::Other(Box::new(err))
+    }
+}
+
 /// Errors that can happen when refreshing an access token.
 ///
 /// This is usually only returned by [`Client::refresh_access_token()`], unless
diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs
index 04a1635c75f..694ca325e3e 100644
--- a/crates/matrix-sdk/src/room/mod.rs
+++ b/crates/matrix-sdk/src/room/mod.rs
@@ -52,6 +52,7 @@ use ruma::{
     },
     assign,
     events::{
+        beacon_info::BeaconInfoEventContent,
         call::notify::{ApplicationType, CallNotifyEventContent, NotifyType},
         direct::DirectEventContent,
         marked_unread::MarkedUnreadEventContent,
@@ -97,7 +98,7 @@ use crate::{
     attachment::AttachmentConfig,
     client::WeakClient,
     config::RequestConfig,
-    error::WrongRoomState,
+    error::{BeaconError, WrongRoomState},
     event_cache::{self, EventCacheDropHandles, RoomEventCache},
     event_handler::{EventHandler, EventHandlerDropGuard, EventHandlerHandle, SyncEvent},
     media::{MediaFormat, MediaRequest},
@@ -2668,6 +2669,69 @@ impl Room {
         Ok(())
     }
 
+    /// Start sharing live location in the room.
+    ///
+    /// # Arguments
+    ///
+    /// * `duration_millis` - The duration for which the live location is
+    ///   shared, in milliseconds.
+    /// * `description` - An optional description for the live location share.
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if the room is not joined or if the state event could
+    /// not be sent.
+    pub async fn start_live_location_share(
+        &self,
+        duration_millis: u64,
+        description: Option<String>,
+    ) -> Result<send_state_event::v3::Response> {
+        self.ensure_room_joined()?;
+
+        self.send_state_event_for_key(
+            self.own_user_id(),
+            BeaconInfoEventContent::new(
+                description,
+                Duration::from_millis(duration_millis),
+                true,
+                None,
+            ),
+        )
+        .await
+    }
+
+    /// Stop sharing live location in the room.
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if the room is not joined, if the beacon information
+    /// is redacted or stripped, or if the state event is not found.
+    pub async fn stop_live_location_share(
+        &self,
+    ) -> Result<send_state_event::v3::Response, BeaconError> {
+        self.ensure_room_joined()?;
+
+        if let Some(raw_event) = self
+            .get_state_event_static_for_key::<BeaconInfoEventContent, _>(self.own_user_id())
+            .await?
+        {
+            match raw_event.deserialize() {
+                Ok(SyncOrStrippedState::Sync(SyncStateEvent::Original(beacon_info))) => {
+                    let mut content = beacon_info.content.clone();
+                    content.stop();
+                    Ok(self.send_state_event_for_key(self.own_user_id(), content).await?)
+                }
+                Ok(SyncOrStrippedState::Sync(SyncStateEvent::Redacted(_))) => {
+                    Err(BeaconError::Redacted)
+                }
+                Ok(SyncOrStrippedState::Stripped(_)) => Err(BeaconError::Stripped),
+                Err(e) => Err(BeaconError::Deserialization(e)),
+            }
+        } else {
+            Err(BeaconError::NotFound)
+        }
+    }
+
     /// Send a call notification event in the current room.
     ///
     /// This is only supposed to be used in **custom** situations where the user
diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs
index 865870dcbe8..71cb1d3b73b 100644
--- a/crates/matrix-sdk/tests/integration/room/joined.rs
+++ b/crates/matrix-sdk/tests/integration/room/joined.rs
@@ -4,12 +4,13 @@ use std::{
 };
 
 use futures_util::future::join_all;
+use js_int::uint;
 use matrix_sdk::{
     config::SyncSettings,
     room::{edit::EditedContent, Receipts, ReportedContentScore, RoomMemberRole},
     test_utils::events::EventFactory,
 };
-use matrix_sdk_base::RoomState;
+use matrix_sdk_base::{deserialized_responses::AnySyncOrStrippedState, RoomState};
 use matrix_sdk_test::{
     async_test, test_json, test_json::sync::CUSTOM_ROOM_POWER_LEVELS, EphemeralTestEvent,
     GlobalAccountDataTestEvent, JoinedRoomBuilder, SyncResponseBuilder, DEFAULT_TEST_ROOM_ID,
@@ -18,11 +19,13 @@ use ruma::{
     api::client::{membership::Invite3pidInit, receipt::create_receipt::v3::ReceiptType},
     assign, event_id,
     events::{
+        location::AssetType,
         receipt::ReceiptThread,
         room::message::{RoomMessageEventContent, RoomMessageEventContentWithoutRelation},
-        TimelineEventType,
+        AnySyncStateEvent, StateEventType, TimelineEventType,
     },
-    int, mxc_uri, owned_event_id, room_id, thirdparty, user_id, OwnedUserId, TransactionId,
+    int, mxc_uri, owned_event_id, room_id, thirdparty, user_id, MilliSecondsSinceUnixEpoch,
+    OwnedUserId, TransactionId,
 };
 use serde_json::{json, Value};
 use wiremock::{
@@ -768,3 +771,235 @@ async fn test_make_reply_event_doesnt_require_event_cache() {
     // make_edit_event works, even if the event cache hasn't been enabled.
     room.make_edit_event(resp_event_id, EditedContent::RoomMessage(new_content)).await.unwrap();
 }
+
+#[async_test]
+async fn test_start_live_location_share_for_room() {
+    let (client, server) = logged_in_client_with_server().await;
+
+    // Validate request body and response, partial body matching due to
+    // auto-generated `org.matrix.msc3488.ts`.
+    Mock::given(method("PUT"))
+        .and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/org.matrix.msc3672.beacon_info/.*"))
+        .and(header("authorization", "Bearer 1234"))
+        .and(body_partial_json(json!({
+            "description": "Live Share",
+            "live": true,
+            "timeout": 3000,
+            "org.matrix.msc3488.asset": { "type": "m.self" }
+        })))
+        .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID))
+        .mount(&server)
+        .await;
+
+    let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
+
+    mock_sync(&server, &*test_json::SYNC, None).await;
+
+    client.sync_once(sync_settings.clone()).await.unwrap();
+
+    let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap();
+
+    let response =
+        room.start_live_location_share(3000, Some("Live Share".to_owned())).await.unwrap();
+
+    assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id);
+    server.reset().await;
+
+    mock_sync(
+        &server,
+        json!({
+            "next_batch": "s526_47314_0_7_1_1_1_1_1",
+            "rooms": {
+                "join": {
+                    *DEFAULT_TEST_ROOM_ID: {
+                        "state": {
+                            "events": [
+                                {
+                                    "content": {
+                                        "description": "Live Share",
+                                        "live": true,
+                                        "org.matrix.msc3488.ts": 1_636_829_458,
+                                        "timeout": 3000,
+                                        "org.matrix.msc3488.asset": { "type": "m.self" }
+                                    },
+                                    "event_id": "$15139375514XsgmR:localhost",
+                                    "origin_server_ts": 1_636_829_458,
+                                    "sender": "@example:localhost",
+                                    "state_key": "@example:localhost",
+                                    "type": "org.matrix.msc3672.beacon_info",
+                                    "unsigned": {
+                                        "age": 7034220
+                                    }
+                                },
+                            ]
+                        }
+                    }
+                }
+            }
+
+        }),
+        None,
+    )
+    .await;
+
+    client.sync_once(sync_settings.clone()).await.unwrap();
+    server.reset().await;
+
+    let state_events = room.get_state_events(StateEventType::BeaconInfo).await.unwrap();
+    assert_eq!(state_events.len(), 1);
+
+    let raw_event = state_events.first().expect("There should be a beacon_info state event");
+
+    let ev = match raw_event.deserialize().expect("Failed to deserialize event") {
+        AnySyncOrStrippedState::Sync(AnySyncStateEvent::BeaconInfo(ev)) => ev,
+        _ => panic!("Expected a BeaconInfo event"),
+    };
+
+    let content = ev.as_original().unwrap().content.clone();
+
+    assert_eq!(ev.sender(), room.own_user_id());
+    assert_eq!(ev.state_key(), "@example:localhost");
+    assert_eq!(ev.event_id(), event_id!("$15139375514XsgmR:localhost"));
+    assert_eq!(ev.event_type(), StateEventType::BeaconInfo);
+    assert_eq!(ev.origin_server_ts(), MilliSecondsSinceUnixEpoch(uint!(1_636_829_458)));
+
+    assert_eq!(content.description, Some("Live Share".to_owned()));
+    assert_eq!(content.timeout, Duration::from_millis(3000));
+    assert_eq!(content.ts, MilliSecondsSinceUnixEpoch(uint!(1_636_829_458)));
+    assert_eq!(content.asset.type_, AssetType::Self_);
+
+    assert!(content.live);
+}
+
+#[async_test]
+async fn test_stop_sharing_live_location() {
+    let (client, server) = logged_in_client_with_server().await;
+
+    // Validate request body and response, partial body matching due to
+    // auto-generated `org.matrix.msc3488.ts`.
+    Mock::given(method("PUT"))
+        .and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/org.matrix.msc3672.beacon_info/.*"))
+        .and(header("authorization", "Bearer 1234"))
+        .and(body_partial_json(json!({
+            "description": "Live Share",
+            "live": false,
+            "timeout": 3000,
+            "org.matrix.msc3488.asset": { "type": "m.self" }
+        })))
+        .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID))
+        .mount(&server)
+        .await;
+
+    let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
+
+    mock_sync(
+        &server,
+        json!({
+            "next_batch": "s526_47314_0_7_1_1_1_1_1",
+            "rooms": {
+                "join": {
+                    *DEFAULT_TEST_ROOM_ID: {
+                        "state": {
+                            "events": [
+                                {
+                                    "content": {
+                                        "description": "Live Share",
+                                        "live": true,
+                                        "org.matrix.msc3488.ts": 1_636_829_458,
+                                        "timeout": 3000,
+                                        "org.matrix.msc3488.asset": { "type": "m.self" }
+                                    },
+                                    "event_id": "$15139375514XsgmR:localhost",
+                                    "origin_server_ts": 1_636_829_458,
+                                    "sender": "@example:localhost",
+                                    "state_key": "@example:localhost",
+                                    "type": "org.matrix.msc3672.beacon_info",
+                                    "unsigned": {
+                                        "age": 7034220
+                                    }
+                                },
+                            ]
+                        }
+                    }
+                }
+            }
+
+        }),
+        None,
+    )
+    .await;
+
+    client.sync_once(sync_settings.clone()).await.unwrap();
+
+    let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap();
+
+    let response = room.stop_live_location_share().await.unwrap();
+
+    assert_eq!(event_id!("$h29iv0s8:example.com"), response.event_id);
+    server.reset().await;
+
+    mock_sync(
+        &server,
+        json!({
+            "next_batch": "s526_47314_1_7_1_1_1_1_1",
+            "rooms": {
+                "join": {
+                    *DEFAULT_TEST_ROOM_ID: {
+                        "state": {
+                            "events": [
+                                {
+                                    "content": {
+                                        "description": "Live Share",
+                                        "live": false,
+                                        "org.matrix.msc3488.ts": 1_636_829_458,
+                                        "timeout": 3000,
+                                        "org.matrix.msc3488.asset": { "type": "m.self" }
+                                    },
+                                    "event_id": "$15139375514XsgmR:localhost",
+                                    "origin_server_ts": 1_636_829_458,
+                                    "sender": "@example:localhost",
+                                    "state_key": "@example:localhost",
+                                    "type": "org.matrix.msc3672.beacon_info",
+                                    "unsigned": {
+                                        "age": 7034220
+                                    }
+                                },
+                            ]
+                        }
+                    }
+                }
+            }
+
+        }),
+        None,
+    )
+    .await;
+
+    client.sync_once(sync_settings.clone()).await.unwrap();
+    server.reset().await;
+
+    let state_events = room.get_state_events(StateEventType::BeaconInfo).await.unwrap();
+    assert_eq!(state_events.len(), 1);
+
+    let raw_event = state_events.first().expect("There should be a beacon_info state event");
+
+    let ev = match raw_event.deserialize().expect("Failed to deserialize event") {
+        AnySyncOrStrippedState::Sync(AnySyncStateEvent::BeaconInfo(ev)) => ev,
+        _ => panic!("Expected a BeaconInfo event"),
+    };
+
+    let content = ev.as_original().unwrap().content.clone();
+
+    assert_eq!(ev.sender(), room.own_user_id());
+    assert_eq!(ev.state_key(), "@example:localhost");
+    assert_eq!(ev.event_id(), event_id!("$15139375514XsgmR:localhost"));
+    assert_eq!(ev.event_type(), StateEventType::BeaconInfo);
+    assert_eq!(ev.origin_server_ts(), MilliSecondsSinceUnixEpoch(uint!(1_636_829_458)));
+
+    assert_eq!(content.description, Some("Live Share".to_owned()));
+    assert_eq!(content.timeout, Duration::from_millis(3000));
+    assert_eq!(content.ts, MilliSecondsSinceUnixEpoch(uint!(1_636_829_458)));
+    assert_eq!(content.asset.type_, AssetType::Self_);
+
+    assert!(!content.live);
+}