diff --git a/services/identity/src/database/device_list.rs b/services/identity/src/database/device_list.rs
--- a/services/identity/src/database/device_list.rs
+++ b/services/identity/src/database/device_list.rs
@@ -144,6 +144,22 @@
       last_primary_signature: update_info.last_signature.clone(),
     }
   }
+
+  pub fn has_device(&self, device_id: &String) -> bool {
+    self.device_ids.contains(device_id)
+  }
+
+  pub fn is_primary_device(&self, device_id: &String) -> bool {
+    self
+      .device_ids
+      .first()
+      .filter(|it| *it == device_id)
+      .is_some()
+  }
+
+  pub fn has_secondary_device(&self, device_id: &String) -> bool {
+    self.has_device(device_id) && !self.is_primary_device(device_id)
+  }
 }
 
 // helper structs for converting to/from attribute values for sort key (a.k.a itemID)
@@ -816,6 +832,37 @@
     Ok(())
   }
 
+  /// Removes device data from devices table. If the device doesn't exist,
+  /// it is a no-op. This does not update the device list; the device ID
+  /// should be removed from the device list separately.
+  #[tracing::instrument(skip_all)]
+  pub async fn remove_device_data(
+    &self,
+    user_id: impl Into<String>,
+    device_id: impl Into<String>,
+  ) -> Result<(), Error> {
+    let user_id = user_id.into();
+    let device_id = device_id.into();
+
+    self
+      .client
+      .delete_item()
+      .table_name(devices_table::NAME)
+      .key(ATTR_USER_ID, AttributeValue::S(user_id))
+      .key(ATTR_ITEM_ID, DeviceIDAttribute(device_id).into())
+      .send()
+      .await
+      .map_err(|e| {
+        error!(
+          errorType = error_types::DEVICE_LIST_DB_LOG,
+          "Failed to delete device data: {:?}", e
+        );
+        Error::AwsSdk(e.into())
+      })?;
+
+    Ok(())
+  }
+
   /// Registers primary device for user, stores its signed device list
   pub async fn register_primary_device(
     &self,
diff --git a/services/identity/src/database/token.rs b/services/identity/src/database/token.rs
--- a/services/identity/src/database/token.rs
+++ b/services/identity/src/database/token.rs
@@ -148,17 +148,19 @@
 
   pub async fn delete_access_token_data(
     &self,
-    user_id: String,
-    device_id_key: String,
+    user_id: impl Into<String>,
+    device_id_key: impl Into<String>,
   ) -> Result<(), Error> {
     use crate::constants::token_table::*;
+    let user_id = user_id.into();
+    let device_id = device_id_key.into();
 
     self
       .client
       .delete_item()
       .table_name(NAME)
       .key(PARTITION_KEY.to_string(), AttributeValue::S(user_id))
-      .key(SORT_KEY.to_string(), AttributeValue::S(device_id_key))
+      .key(SORT_KEY.to_string(), AttributeValue::S(device_id))
       .send()
       .await
       .map_err(|e| Error::AwsSdk(e.into()))?;
diff --git a/services/identity/src/grpc_services/authenticated.rs b/services/identity/src/grpc_services/authenticated.rs
--- a/services/identity/src/grpc_services/authenticated.rs
+++ b/services/identity/src/grpc_services/authenticated.rs
@@ -337,7 +337,40 @@
     &self,
     request: tonic::Request<Empty>,
   ) -> Result<tonic::Response<Empty>, tonic::Status> {
-    Err(tonic::Status::unimplemented(""))
+    let (user_id, device_id) = get_user_and_device_id(&request)?;
+
+    debug!(
+      "Secondary device logout request for user_id={}, device_id={}",
+      user_id, device_id
+    );
+    self
+      .verify_device_on_device_list(
+        &user_id,
+        &device_id,
+        DeviceListItemKind::Secondary,
+      )
+      .await?;
+
+    self
+      .db_client
+      .delete_access_token_data(&user_id, &device_id)
+      .await
+      .map_err(handle_db_error)?;
+
+    self
+      .db_client
+      .remove_device_data(&user_id, &device_id)
+      .await
+      .map_err(handle_db_error)?;
+
+    self
+      .db_client
+      .delete_otks_table_rows_for_user_device(&user_id, &device_id)
+      .await
+      .map_err(handle_db_error)?;
+
+    let response = Empty {};
+    Ok(Response::new(response))
   }
 
   #[tracing::instrument(skip_all)]
@@ -645,6 +678,60 @@
   }
 }
 
+enum DeviceListItemKind {
+  Any,
+  Primary,
+  Secondary,
+}
+
+impl AuthenticatedService {
+  async fn verify_device_on_device_list(
+    &self,
+    user_id: &String,
+    device_id: &String,
+    device_kind: DeviceListItemKind,
+  ) -> Result<(), tonic::Status> {
+    let device_list = self
+      .db_client
+      .get_current_device_list(user_id)
+      .await
+      .map_err(|err| {
+        error!(
+          user_id,
+          errorType = error_types::GRPC_SERVICES_LOG,
+          "Failed fetching device list: {err}"
+        );
+        handle_db_error(err)
+      })?;
+
+    let Some(device_list) = device_list else {
+      error!(
+        user_id,
+        errorType = error_types::GRPC_SERVICES_LOG,
+        "User has no device list!"
+      );
+      return Err(Status::failed_precondition("no device list"));
+    };
+
+    use DeviceListItemKind as DeviceKind;
+    let device_on_list = match device_kind {
+      DeviceKind::Any => device_list.has_device(device_id),
+      DeviceKind::Primary => device_list.is_primary_device(device_id),
+      DeviceKind::Secondary => device_list.has_secondary_device(device_id),
+    };
+
+    if !device_on_list {
+      debug!(
+        "Device {} not on device list for user {}",
+        device_id, user_id
+      );
+      return Err(Status::permission_denied("device not on device list"));
+    }
+
+    Ok(())
+  }
+}
+
 #[derive(Clone, serde::Serialize, serde::Deserialize)]
 pub struct DeletePasswordUserInfo {
   pub opaque_server_login: comm_opaque2::server::Login,